package smartgrid;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.jgrapht.UndirectedGraph;
import org.jgrapht.VertexFactory;
import org.jgrapht.alg.FloydWarshallShortestPaths;
import org.jgrapht.generate.CompleteGraphGenerator;
import org.jgrapht.generate.GridGraphGenerator;
import org.jgrapht.generate.ScaleFreeGraphGenerator;
import org.jgrapht.graph.DefaultEdge;
import org.jgrapht.graph.SimpleGraph;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import smartgrid.agents.broker.IBrokerAgent;
import smartgrid.agents.prosumer.IProsumerAgent;
import smartgrid.agents.provider.IProviderAgent;
import smartgrid.agents.regulator.IRegulatorAgent;
import smartgrid.conf.BrokerConf;
import smartgrid.conf.SmartGridConf;
import smartgrid.conf.GeneralConf;
import smartgrid.conf.ProsumerConf;
import smartgrid.conf.ProviderConf;
import smartgrid.conf.RegulatorConf;
import smartgrid.modules.normative.sanction.SanctionAbstract;
import smartgrid.modules.normative.sanction.SanctionAbstract.Type;
import smartgrid.modules.trade.Contract;
import smartgrid.modules.trade.Demand;
import smartgrid.modules.trade.Offer;
import smartgrid.output.OutputController;
import smartgrid.output.AbstractEntity.EntityType;
import smartgrid.util.random.RandomUtil;

public class SmartGridSim {
	
	private static final Logger	logger	= LoggerFactory
																					.getLogger(SmartGridSim.class);
	
	private enum Networks {
		COMPLETE,
		LATTICE,
		SCALEFREE;
	}
	
	// Variables
	private GeneralConf										generalConf;
	
	private RegulatorConf									regulatorConf;
	
	private IRegulatorAgent								regulator;
	
	private ProviderConf									providerConf;
	
	private IProviderAgent								provider;
	
	private BrokerConf										brokerConf;
	
	private IBrokerAgent									broker;
	
	private List<ProsumerConf>						prosumersConf;
	
	private Map<Integer, IProsumerAgent>	prosumers;
	
	
	/**
	 * Constructor
	 * 
	 * @param confParser
	 *          Configuration parameters
	 * @return none
	 */
	public SmartGridSim(SmartGridConf parser) {
		// Configuration
		this.generalConf = parser.getGeneralConf();
		this.regulatorConf = parser.getRegulatorConf();
		this.providerConf = parser.getProviderConf();
		this.brokerConf = parser.getBrokerConf();
		this.prosumersConf = parser.getProsumersConf();
	}
	
	
	/**
	 * Initialization
	 * 
	 * @param none
	 * @return none
	 */
	public void init(int seed) {
		int id = 0;
		
		// Initialize Prosumers
		createProsumers(id);
		
		// Initialize Regulator
		createRegulator(id++);
		
		// Initialize Provider
		createProvider(id++);
		
		// Initialize Broker
		createBroker(id++);
		
		// Create a scale-free network
		UndirectedGraph<Integer, DefaultEdge> network = new SimpleGraph<Integer, DefaultEdge>(
				DefaultEdge.class);
		
		switch(Networks.valueOf(this.generalConf.getNetwork())) {
			case COMPLETE:
				CompleteGraphGenerator<Integer, DefaultEdge> complete = new CompleteGraphGenerator<Integer, DefaultEdge>(
						this.prosumers.size());
				complete.generateGraph(network, new VertexFactory<Integer>() {
					
					int	last	= 0;
					
					
					public Integer createVertex() {
						return new Integer(this.last++);
					}
				}, null);
				break;
			case LATTICE:
				int rows = (int) Math.sqrt(this.prosumers.size());
				int cols = (int) Math.sqrt(this.prosumers.size());
				GridGraphGenerator<Integer, DefaultEdge> grid = new GridGraphGenerator<Integer, DefaultEdge>(
						rows, cols);
				grid.generateGraph(network, new VertexFactory<Integer>() {
					
					int	last	= 0;
					
					
					public Integer createVertex() {
						return new Integer(this.last++);
					}
				}, null);
				break;
			case SCALEFREE:
				ScaleFreeGraphGenerator<Integer, DefaultEdge> scaleFree = new ScaleFreeGraphGenerator<Integer, DefaultEdge>(
						this.prosumers.size(), seed);
				scaleFree.generateGraph(network, new VertexFactory<Integer>() {
					
					int	last	= 0;
					
					
					public Integer createVertex() {
						return new Integer(this.last++);
					}
				}, null);
				break;
			default:
				ScaleFreeGraphGenerator<Integer, DefaultEdge> defG = new ScaleFreeGraphGenerator<Integer, DefaultEdge>(
						this.prosumers.size());
				defG.generateGraph(network, new VertexFactory<Integer>() {
					
					int	last	= 0;
					
					
					public Integer createVertex() {
						return new Integer(this.last++);
					}
				}, null);
				break;
		}
		
		FloydWarshallShortestPaths<Integer, DefaultEdge> paths = new FloydWarshallShortestPaths<Integer, DefaultEdge>(
				network);
		
		// Create the map of Prosumers and neighbors
		Map<Integer, List<IProsumerAgent>> neighborsList = new HashMap<Integer, List<IProsumerAgent>>();
		for(Integer prosumerId : this.prosumers.keySet()) {
			Set<DefaultEdge> edges = network.edgesOf(prosumerId);
			
			String logStr = "[NEIGHBOR] [" + prosumerId + "][";
			
			List<IProsumerAgent> neighbors = new ArrayList<IProsumerAgent>();
			for(DefaultEdge edge : edges) {
				Integer neighborId = network.getEdgeTarget(edge);
				if (neighborId.equals(prosumerId)) {
					neighborId = network.getEdgeSource(edge);
				}
				
				neighbors.add(this.prosumers.get(neighborId));
				
				logStr += neighborId + ";";
			}
			
			neighborsList.put(prosumerId, neighbors);
			
			logger.debug(logStr.substring(0, (logStr.length() - 1)) + "]");
		}
		
		// Setup the neighborsList and prosumers to all Prosumers
		for(Integer prosumerId : this.prosumers.keySet()) {
			IProsumerAgent prosumer = this.prosumers.get(prosumerId);
			prosumer.setNeighbors(neighborsList);
			prosumer.setProsumers(this.prosumers);
		}
		
		// Set up the number of hops among the Prosumers
		Map<Integer, Map<Integer, Double>> distances = new HashMap<Integer, Map<Integer, Double>>();
		Map<Integer, Double> distance;
		for(Integer i : this.prosumers.keySet()) {
			
			distance = new HashMap<Integer, Double>();
			for(Integer j : this.prosumers.keySet()) {
				if (!i.equals(j)) {
					distance.put(j, paths.shortestDistance(i, j));
					
					logger.debug("[DISTANCE] [" + i + ";" + j + ";" + distance.get(j)
							+ "]");
				}
			}
			
			distances.put(i, distance);
		}
		
		for(Integer i : this.prosumers.keySet()) {
			IProsumerAgent prosumer = this.prosumers.get(i);
			prosumer.setDistance(distances);
		}
		
		for(Integer i : this.prosumers.keySet()) {
			IProsumerAgent prosumer = this.prosumers.get(i);
			prosumer.initSim();
		}
	}
	
	
	/**
	 * Create Regulator agent
	 * 
	 * @param id
	 *          Regulator identification
	 * @return none
	 */
	private void createRegulator(int id) {
		try {
			@SuppressWarnings("unchecked")
			Class<IRegulatorAgent> nbClass = (Class<IRegulatorAgent>) Class
					.forName(regulatorConf.getClassName());
			
			Constructor<IRegulatorAgent> nbConstructor = nbClass
					.getDeclaredConstructor(Integer.class, RegulatorConf.class);
			
			this.regulator = nbConstructor.newInstance(id, this.regulatorConf);
			
		} catch(ClassNotFoundException e) {
			logger.debug(e.toString());
		} catch(NoSuchMethodException e) {
			logger.debug(e.toString());
		} catch(InvocationTargetException e) {
			logger.debug(e.toString());
		} catch(IllegalAccessException e) {
			logger.debug(e.toString());
		} catch(InstantiationException e) {
			logger.debug(e.toString());
		}
	}
	
	
	/**
	 * Create Provider agent
	 * 
	 * @param id
	 *          Provider identification
	 * @return none
	 */
	private void createProvider(int id) {
		try {
			@SuppressWarnings("unchecked")
			Class<IProviderAgent> nbClass = (Class<IProviderAgent>) Class
					.forName(this.providerConf.getClassName());
			
			Constructor<IProviderAgent> nbConstructor = nbClass
					.getDeclaredConstructor(Integer.class, ProviderConf.class);
			
			this.provider = nbConstructor.newInstance(id, this.providerConf);
			
		} catch(ClassNotFoundException e) {
			logger.debug(e.toString());
		} catch(NoSuchMethodException e) {
			logger.debug(e.toString());
		} catch(InvocationTargetException e) {
			logger.debug(e.toString());
		} catch(IllegalAccessException e) {
			logger.debug(e.toString());
		} catch(InstantiationException e) {
			logger.debug(e.toString());
		}
	}
	
	
	/**
	 * Create Broker agent
	 * 
	 * @param id
	 *          Broker identification
	 * @return none
	 */
	private void createBroker(int id) {
		try {
			@SuppressWarnings("unchecked")
			Class<IBrokerAgent> nbClass = (Class<IBrokerAgent>) Class
					.forName(this.brokerConf.getClassName());
			
			Constructor<IBrokerAgent> nbConstructor = nbClass.getDeclaredConstructor(
					Integer.class, BrokerConf.class);
			
			this.broker = nbConstructor.newInstance(id, this.brokerConf);
			
		} catch(ClassNotFoundException e) {
			logger.debug(e.toString());
		} catch(NoSuchMethodException e) {
			logger.debug(e.toString());
		} catch(InvocationTargetException e) {
			logger.debug(e.toString());
		} catch(IllegalAccessException e) {
			logger.debug(e.toString());
		} catch(InstantiationException e) {
			logger.debug(e.toString());
		}
	}
	
	
	/**
	 * Create Prosumer agents
	 * 
	 * @param none
	 * @return none
	 */
	private void createProsumers(int id) {
		try {
			List<Integer> prosumerList = new ArrayList<Integer>();
			Map<Integer, IProsumerAgent> auxProsumers = new LinkedHashMap<Integer, IProsumerAgent>();
			for(ProsumerConf prosumerConf : this.prosumersConf) {
				
				@SuppressWarnings("unchecked")
				Class<IProsumerAgent> nbClass = (Class<IProsumerAgent>) Class
						.forName(prosumerConf.getClassName());
				
				Constructor<IProsumerAgent> nbConstructor = nbClass
						.getDeclaredConstructor(Integer.class, ProsumerConf.class,
								String.class);
				
				IProsumerAgent prosumer;
				for(int i = 0; i < prosumerConf.getQty(); i++) {
					prosumer = nbConstructor.newInstance(id, prosumerConf,
							this.generalConf.getFilenameConf().getProsumer());
					auxProsumers.put(id, prosumer);
					prosumerList.add(id);
					id++;
				}
			}
			
			Collections.shuffle(prosumerList);
			
			this.prosumers = new LinkedHashMap<Integer, IProsumerAgent>();
			for(Integer prosumerId : prosumerList) {
				this.prosumers.put(prosumerId, auxProsumers.get(prosumerId));
			}
			
		} catch(ClassNotFoundException e) {
			logger.debug(e.toString());
		} catch(NoSuchMethodException e) {
			logger.debug(e.toString());
		} catch(InvocationTargetException e) {
			logger.debug(e.toString());
		} catch(IllegalAccessException e) {
			logger.debug(e.toString());
		} catch(InstantiationException e) {
			logger.debug(e.toString());
		}
	}
	
	
	/**
	 * Simulation execution
	 * 
	 * @param none
	 * @return none
	 */
	public void execute() {
		
		int sIndex = 0;
		List<Integer> seeds = this.generalConf.getSeeds();
		for(int replica = 0; replica < this.generalConf.getReplications(); replica++) {
			OutputController.getInstance(this.generalConf.getOutputConf(), replica);
			
			// Output
			OutputController.init(EntityType.CONTRACT, this.generalConf
					.getFilenameConf().getBroker());
			OutputController.init(EntityType.PROVIDER, this.generalConf
					.getFilenameConf().getProvider());
			OutputController.init(EntityType.REGULATOR, this.generalConf
					.getFilenameConf().getRegulator());
			
			if (sIndex > (seeds.size() - 1)) {
				sIndex = 0;
			}
			int seed = seeds.get(sIndex++);
			RandomUtil.setSeed(seed);
			
			// Initialize the agents
			this.init(seed);
			
			IProsumerAgent prosumer;
			List<Contract> contracts = new ArrayList<Contract>();
			Map<Integer, List<SanctionAbstract>> sanctionsSender = new HashMap<Integer, List<SanctionAbstract>>();
			Map<Integer, List<SanctionAbstract>> sanctionsReceiver = new HashMap<Integer, List<SanctionAbstract>>();
			
			int timeToWrite = this.generalConf.getOutputConf().getTimeToWrite();
			int timeCounting = timeToWrite;
			
			int numIterations = this.generalConf.getIterations();
			for(int i = 0; i < numIterations; i++) {
				
				/*
				 * Phase #0 - Initialization
				 */
				logger.debug("Timestep " + i);
				
				contracts.clear();
				sanctionsSender.clear();
				sanctionsReceiver.clear();
				
				this.regulator.initTimestep(i);
				this.provider.initTimestep(i);
				this.broker.initTimestep(i);
				
				/*
				 * Phase #1 - Trading
				 */
				
				// Prosumers offer the forecasted energy
				for(int id : this.prosumers.keySet()) {
					prosumer = this.prosumers.get(id);
					prosumer.initTimestep(i);
					prosumer.estimationConGen();
					if (prosumer.hasExcessEnergyEstimation()) {
						Offer offer = prosumer.getOffer();
						if (offer != null) {
							logger.debug(offer.toString());
							
							this.broker.addOffer(offer);
						}
					}
				}
				
				// Prosumers demand energy
				Map<Integer, Offer> offers = this.broker.getOffers();
				for(int id : this.prosumers.keySet()) {
					prosumer = this.prosumers.get(id);
					if (!prosumer.hasExcessEnergyEstimation()) {
						Demand demand = prosumer.getDemand(offers);
						if (demand != null) {
							logger.debug(demand.toString());
							
							this.broker.addDemand(demand);
						}
					}
				}
				
				// Broker performs the auction
				contracts = this.broker.auction();
				
				// Prosumers are informed about the auction and committed contracts
				for(Contract contract : contracts) {
					prosumer = this.prosumers.get(contract.getSupplier());
					prosumer.setContract(contract);
					
					prosumer = this.prosumers.get(contract.getBuyer());
					prosumer.setContract(contract);
				}
				
				/*
				 * Phase #2 - Prosumers play among themselves
				 */
				
				// Prosumers consume and generate real energy
				for(Integer id : this.prosumers.keySet()) {
					prosumer = this.prosumers.get(id);
					prosumer.realConGen();
				}
				
				// Update contracts of Prosumers
				for(Contract contract : contracts) {
					prosumer = this.prosumers.get(contract.getSupplier());
					prosumer.updateContract();
				}
				
				// Update the Prosumer's wealth
				for(Integer id : this.prosumers.keySet()) {
					prosumer = this.prosumers.get(id);
					prosumer.updateWealth();
				}
				
				// Provider balances the network
				for(Integer id : this.prosumers.keySet()) {
					prosumer = this.prosumers.get(id);
					double balance = prosumer.energyBalance();
					double totalPrice = 0.0;
					
					// Prosumer buys energy from Provider
					if (balance < 0) {
						balance = Math.abs(balance);
						totalPrice = this.provider.sellEnergy(id, balance);
						prosumer.providerTrading(true, balance, totalPrice);
						
						// Prosumer sells energy to Provider
					} else if (balance > 0) {
						totalPrice = this.provider.buyEnergy(id, balance);
						prosumer.providerTrading(false, balance, totalPrice);
					}
				}
				
				/*
				 * Phase #3 - Sanction decision
				 */
				// Prosumers evaluate partners in the contract
				for(Integer id : this.prosumers.keySet()) {
					prosumer = this.prosumers.get(id);
					prosumer.evaluateContract();
				}
				
				// Prosumers decide to sanction or denounce
				Map<Integer, List<SanctionAbstract>> denounceList = new HashMap<Integer, List<SanctionAbstract>>();
				for(Integer id : this.prosumers.keySet()) {
					prosumer = this.prosumers.get(id);
					
					List<SanctionAbstract> sanctionList;
					for(SanctionAbstract sanction : prosumer.decideSanction()) {
						
						int receiver = sanction.getReceiver();
						
						if (sanction.getType().equals(Type.DENOUNCE)) {
							
							// Denounce
							List<SanctionAbstract> denounces;
							if (denounceList.containsKey(receiver)) {
								denounces = denounceList.get(receiver);
							} else {
								denounces = new ArrayList<SanctionAbstract>();
							}
							denounces.add(sanction);
							denounceList.put(receiver, denounces);
							
							// Sender
							int sender = sanction.getSender();
							if (sanctionsSender.containsKey(sender)) {
								sanctionList = sanctionsSender.get(sender);
							} else {
								sanctionList = new ArrayList<SanctionAbstract>();
							}
							sanctionList.add(sanction);
							sanctionsSender.put(sender, sanctionList);
							
						} else {
							
							// Receiver
							if (sanctionsReceiver.containsKey(receiver)) {
								sanctionList = sanctionsReceiver.get(receiver);
							} else {
								sanctionList = new ArrayList<SanctionAbstract>();
							}
							sanctionList.add(sanction);
							sanctionsReceiver.put(receiver, sanctionList);
							
							// Sender
							int sender = sanction.getSender();
							if (sanctionsSender.containsKey(sender)) {
								sanctionList = sanctionsSender.get(sender);
							} else {
								sanctionList = new ArrayList<SanctionAbstract>();
							}
							sanctionList.add(sanction);
							sanctionsSender.put(sender, sanctionList);
						}
					}
				}
				
				// Regulator processes denounces
				List<SanctionAbstract> sanctionList;
				for(SanctionAbstract sanction : this.regulator
						.decideSanction(denounceList)) {
					
					int receiver = sanction.getReceiver();
					if (sanctionsReceiver.containsKey(receiver)) {
						sanctionList = sanctionsReceiver.get(receiver);
					} else {
						sanctionList = new ArrayList<SanctionAbstract>();
					}
					
					sanctionList.add(sanction);
					sanctionsReceiver.put(receiver, sanctionList);
				}
				
				// Apply sanctions to the Prosumers
				for(Integer id : sanctionsReceiver.keySet()) {
					prosumer = this.prosumers.get(id);
					
					for(SanctionAbstract sanction : sanctionsReceiver.get(id)) {
						prosumer.handleSanction(sanction);
						
						logger.info(sanction.toString());
					}
				}
				
				/*
				 * Phase #4 - Updating
				 */
				
				// Update strategy information
				for(Integer id : this.prosumers.keySet()) {
					prosumer = this.prosumers.get(id);
					prosumer.updateStrategy(sanctionsSender, sanctionsReceiver);
				}
				
				// Generate the output records
				this.broker.generateOutput();
				this.provider.generateOutput();
				this.regulator.generateOutput();
				
				for(Integer id : this.prosumers.keySet()) {
					prosumer = this.prosumers.get(id);
					prosumer.generateOutput();
				}
				if (timeCounting < 0) {
					// Write data to the file
					try {
						OutputController.write();
					} catch(IOException e) {
						logger.debug(e.toString());
					}
					timeCounting = timeToWrite;
				} else {
					timeCounting--;
				}
			}
		}
		
		try {
			OutputController.close();
		} catch(IOException e) {
			logger.debug(e.toString());
		}
	}
	
	
	/**
	 * Main
	 * 
	 * @param args
	 *          [0] XML configuration file
	 *          [1] XSD configuration file
	 * @return none
	 */
	public static void main(String[] args) {
		
		if (args.length >= 2) {
			if (SmartGridConf.isValid(args[0], args[1])) {
				SmartGridConf parser = SmartGridConf.getConf(args[0], args[1]);
				
				SmartGridSim sim = new SmartGridSim(parser);
				
				long time = System.currentTimeMillis();
				logger.debug("Initialize at " + time);
				
				sim.execute();
				
				logger.debug("Finalize at " + (time - System.currentTimeMillis()));
				
			} else {
				logger.error("Configuration file invalid");
				System.exit(1);
			}
		} else {
			logger.error("Missing initial parameters");
			System.exit(1);
		}
	}
}